import netCDF4
import numpy
from .          import __Conventions__
from .variable  import Variable
from .utils     import equals as utils_equals
from .fieldlist import FieldList

def write(fields, file, format='NETCDF3_CLASSIC', verbose=False):
    '''
    
Write fields to a CF-netCDF file.
    
NetCDF dimension and variable names will be taken from variables'
`ncvar` attributes and the space attribute `nc_dimensions` if present,
otherwise they are inferred from standard names or set to
defaults. NetCDF names may be automatically given a numerical suffix
to avoid duplication.

Output netCDF file global properties are those which occur in the set
of CF global properties and non-standard data variable properties and
which have equal values across all input fields.

Logically identical field components are only written to the file
once, apart from when they need to fulfil both dimension coordinate
and auxiliary coordinate roles for different data variables.

:Parameters:

    fields : (sequence of) Field or FieldList
        The field or fields to write to the file.

    file : str
        The netCDF file to write fields to.

    format : str, optional
        The format of the output netCDF file. Valid formats are those
        supported by the netCDF4 module: 'NETCDF3_CLASSIC',
        'NETCDF3_64BIT' 'NETCDF4_CLASSIC' and 'NETCDF4'. The default
        format is 'NETCDF3_CLASSIC'. Certain write operations may be
        considerably faster using 'NETCDF4_CLASSIC' or 'NETCDF4'.

    verbose : bool, optional
        If True then print information to stdout.

:Returns:

    None

:Raises:

    ValueError :
        If a field does not have information required to write certain
        aspects of a CF-netCDF file.

**Examples**

>>> f
[<CF Field: air_pressure(30, 24)>,
 <CF Field: u_compnt_of_wind(19, 29, 24)>,
 <CF Field: v_compnt_of_wind(19, 29, 24)>,
 <CF Field: potential_temperature(19, 30, 24)>]
>>> write(f, 'file')

>>> type(f)
<class 'cf.field.FieldList'>
>>> type(g)
<class 'cf.field.Field'>
>>> cf.write([f, g], 'file.nc', verbose=True)
[<CF Field: air_pressure(30, 24)>,
 <CF Field: u_compnt_of_wind(19, 29, 24)>,
 <CF Field: v_compnt_of_wind(19, 29, 24)>,
 <CF Field: potential_temperature(19, 30, 24)>]

'''
    # ----------------------------------------------------------------
    # Initialize dictionary of useful global variables
    # ----------------------------------------------------------------
    g = {'netcdf'            : None,    # - netCDF4.Dataset instance
                                        #-----------------------------
         'nc'                : {},      # - Dictionary of
                                        #   netCDF4.Variable instances
                                        #   keyed by their netCDF
                                        #   variable names.
         'dim_to_ncdim'      : {},      # - Dictionary of netCDF
                                        #   dimension names keyed by
                                        #   their field dimension
                                        #   names.
         'dim_to_ncscalar'   : {},      # - Dictionary of netCDF
                                        #   scalar coordinate names
                                        #   keyed by their field
                                        #   dimension names.
         'ncdim_size'        : {},      # - Dictionary of dimension
                                        #   sizes keyed by netCDF
                                        #   dimension names.
         'seen'              : {},      # - Dictionary netCDF variable
                                        #   names and netCDF
                                        #   dimensions keyed by
                                        #   elements of the field
                                        #   (such as a coordinate or a
                                        #   transform).
                                        #-----------------------------
         'ncvar_names'       : set(()), # - set of all netCDF
                                        #   dimension and netCDF
                                        #   variable names.
         'global_properties' : set(()), # - Set of global or
                                        #   non-standard CF properties
                                        #   which have identical
                                        #   values across all input
                                        #   fields.         
                                        #-----------------------------
         'dimN'              : 1,       # - Counter
         'auxN'              : 1,       # - Counter
         'scalarN'           : 1,       # - Counter
         'cmN'               : 1,       # - Counter
         'dataN'             : 1,       # - Counter
         'gmN'               : 1,       # - Counter
         'bndN'              : 1,       # - Counter
         'bnddimN'           : 1,       # - Counter
         'strlenN'           : 1,       # - Counter 
         }

    def _grid_ncdimensions(f, key, g=g):
        '''

Return a tuple of the netCDF names for the dimensions of a coordinate
or cell measures variable.

:Parameters:

    f : Field

    key : str

    g : dict

:Returns:

    out : tuple

'''
        space = f.space
        
        if space[key].ndim == 0:
            return ()
        else:
            return tuple(g['dim_to_ncdim'][dim] 
                         for dim in space.dimensions[key])
    #--- End: def
    
    def _grid_ncvar(f, key, default, counter, g=g):
        '''

:Returns:

    ncvar : str

    counter : int

'''
        variable = f.space[key]
        ncvar = getattr(variable, 'ncvar', variable.name(default=default))

        return _check_name(ncvar, counter)        
    #--- End: def
    
    def _write_dimension(ncdim, f, dim, g=g):
        '''

Write a dimension to the netCDF file.

Updates g['dim_to_ncdim'].

:Parameters:

    ncdim : str
        The netCDF name for the dimension.

    f : Field
   
    dim : str
        The field's space's identifier for the dimension.

    g : dict

:Returns:

     None

'''        
        ncdim_size = g['ncdim_size']

        ncdim_size[ncdim]      = f.space.dimension_sizes[dim]
        g['dim_to_ncdim'][dim] = ncdim

        g['netcdf'].createDimension(ncdim, ncdim_size[ncdim])
    #--- End: def

    def _write_dimension_coordinate(f, key, g=g):
        '''

Write a dimension coordinate and bounds to the netCDF file.

This also writes a new netCDF dimension to the file and, if required,
a new netCDF bounds dimension.

Updates g['dimN'] and g['dim_to_ncdim'], g['seen'].

:Parameters:

    f : Field
   
    key : str

    g : dict

:Returns:

    out : str
        The netCDF name of the dimension coordinate.

'''       
        seen  = g['seen']

        space = f.space
        coord = space[key]

        create = False
        if not _seen(coord, g=g):            
            create = True
        elif seen[id(coord)]['ncdims'] != ():
            if seen[id(coord)]['ncvar'] != seen[id(coord)]['ncdims'][0]:
                # Already seen this coordinate but it was an auxiliary
                # coordinate, so it needs to be created as a dimension
                # coordinate.
                create = True
        #--- End: if

        if create:
            ncdim, g['dimN'] = _grid_ncvar(f, key, 'dim', g['dimN'])

            # Create a new dimension, if it is not a scalar coordinate
            if coord.ndim > 0:
                _write_dimension(ncdim, f, key, g=g)

            ncdimensions = _grid_ncdimensions(f, key)
            
            # If this dimension coordinate has bounds then create the
            # bounds netCDF variable and add the bounds or climatology
            # attribute to the dictionary of extra attributes
            extra = _write_coordinate_bounds(coord, ncdimensions, ncdim, g=g)

            # Create a new dimension coordinate variable
            _create_netcdf_variable(ncdim, ncdimensions, coord, 
                                    extra=extra, g=g)
        else:
            ncdim = seen[id(coord)]['ncvar']
        #--- End: if

        try:    ### ????? why not always do this dch??
            g['dim_to_ncdim'][key] = ncdim
        except KeyError:
            pass

        return ncdim
    #--- End: def

    def _write_scalar_coordinate(f, dim, coordinates, g=g):
        '''

Write a scalar coordinate and bounds to the netCDF file.

It is assumed that the input coordinate is has size 1, but this is not
checked.

If an equal scalar coordinate has already been written to the file
then the input coordinate is not written.

Updates g['scalarN'], g['dim_to_ncscalar'].

:Parameters:

    f : Field
   
    dim : str

    coordinates : list

    g : dict

:Returns:

    coordinates : list
        The updated list of netCDF auxiliary coordinate names.

'''
        coord = f.space[dim]
        coord.squeeze()

        if not _seen(coord, (), g=g):
            ncvar, g['scalarN'] = _grid_ncvar(f, dim, 'scalar', g['scalarN'])
            
            # If this scalar coordinate has bounds then create the
            # bounds netCDF variable and add the bounds or climatology
            # attribute to the dictionary of extra attributes
            extra = _write_coordinate_bounds(coord, (), ncvar, g=g)

            # Create a new auxiliary coordinate variable
            _create_netcdf_variable(ncvar, (), coord, extra=extra, g=g)

            g['dim_to_ncscalar'][dim] = ncvar

        else:
            # This scalar coordinate has already been written to the
            # file
            ncvar = g['seen'][id(coord)]['ncvar']
        #--- End: if

        coordinates.append(ncvar)

        return coordinates
    #--- End: def

    def _write_auxiliary_coordinate(f, key, coordinates, g=g):
        '''

Write an auxiliary coordinate and its bounds to the netCDF file.

If an equal auxiliary coordinate has already been written to the file
then the input coordinate is not written.

Updates g['auxN'].

:Parameters:

    f : Field
   
    key : str

    coordinates : list

    g : dict

:Returns:

    coordinates : list
        The list of netCDF auxiliary coordinate names updated in
        place.

**Examples**

>>> coordinates = _write_auxiliary_coordinate(f, 'aux2', coordinates, g=g)

'''
        coord = f.space[key]
        
        ncdimensions = _grid_ncdimensions(f, key)

        if _seen(coord, ncdimensions, g=g):
            ncvar = g['seen'][id(coord)]['ncvar']
        
        else:
            ncvar, g['auxN'] = _grid_ncvar(f, key, 'aux', g['auxN'])
            
            # If this auxiliary coordinate has bounds then create the
            # bounds netCDF variable and add the bounds or climatology
            # attribute to the dictionary of extra attributes
            extra = _write_coordinate_bounds(coord, ncdimensions, ncvar, g=g)

            # Create a new auxiliary coordinate variable
            _create_netcdf_variable(ncvar, ncdimensions, coord,
                                    extra=extra, g=g)
        #--- End: if

        coordinates.append(ncvar)

        return coordinates
    #--- End: def
  
    def _write_coordinate_bounds(coord, coord_ncdimensions, coord_ncvar, g=g):
        '''

Create a coordinate's bounds netCDF variable, creating a new bounds
netCDF dimension if required. Return the bounds variable's netCDF
variable name.

Updates g['bnddimN'], g['bndN'], g['netcdf'].

:Parameters:

    coord : Coordinate

    coord_ncdimensions : tuple
        The ordered netCDF names of the coordinate's dimensions (which
        do not include the bounds dimension).

     coord_ncvar : str
        The netCDF variable of the coordinate.
    g : dict

:Returns:

    out : dict

**Examples**

>>> extra = _write_coordinate_bounds(c, ('dim2',), g=g)

'''
        extra = {}

        if not (coord.isbounded and coord.bounds.hasData):
            return extra

        # Still here? Then this coordinate has a bounds attribute
        # which contains data.
        bounds = coord.bounds

        size = bounds.shape[-1]

        ncdim, g['bnddimN'] = _check_name('bounds%(size)d' % locals(),
                                          g['bnddimN'],
                                          dimsize=size)

        # Check if this bounds variable has not been previously
        # created.
        ncdimensions = coord_ncdimensions +(ncdim,)        
        if _seen(bounds, ncdimensions, g=g):
            # This bounds variable has been previously created, so no
            # need to do so again.
            ncvar = g['seen'][id(bounds)]['ncvar']

        else:

            # This bounds variable has not been previously created, so
            # create it now.
            ncdim_size = g['ncdim_size']
            if ncdim not in ncdim_size:
                ncdim_size[ncdim] = size
                g['netcdf'].createDimension(ncdim, ncdim_size[ncdim])
            #--- End: if
            
            ncvar = getattr(bounds, 'ncvar', '%s_bounds' % coord_ncvar)
            
            ncvar, g['bndN'] = _check_name(ncvar, g['bndN'])
            
            # Note that, in a field, bounds always have equal units to
            # their parent coordinate
            
            # Create the bounds netCDF variable
            _create_netcdf_variable(ncvar, ncdimensions, bounds,
                                    omit=('units', 'calendar'), g=g)
        #--- End: if

        if getattr(coord, 'climatology', None):
            extra['climatology'] = ncvar
        else:
            extra['bounds'] = ncvar
            
        return extra
    #--- End: def
        
    def _write_cell_measure(f, key, cell_measures, g=g):
        '''

Write an auxiliary coordinate and bounds to the netCDF file.

If an equal cell measure has already been written to the file then the
input coordinate is not written.

Updates g['cmN'].

:Parameters:

    f : Field
        The field containing the cell measure.

    key : str
        The space identifier of the cell measure (e.g. 'cmo').

    cell_measures : list

    g : dict

:Returns:

    cell_measures : list
        The updated list of netCDF cell_measures 'measure: name'
        pairings.

**Examples**

'''
        cell_measure = f.space[key]

        ncdimensions = _grid_ncdimensions(f, key)

        if _seen(cell_measure, ncdimensions, g=g):
            ncvar = g['seen'][id(cell_measure)]['ncvar']
        else:
            ncvar, g['cmN'] = _grid_ncvar(f, key, 'cm', g['cmN'])
            
            if not hasattr(cell_measure, 'measure'):
                raise ValueError(
                    "Can't write a cell measure without a 'measure' attribute")

            _create_netcdf_variable(ncvar, ncdimensions, cell_measure, g=g)
        #--- End: if
                
        # Update the cell_measures list
        cell_measures.append('%s: %s' % (cell_measure.measure, ncvar))

        return cell_measures
    #--- End: def
  
    def _seen(variable, ncdims=None, g=g):
        '''

Return True if a variable is logically equal any variable in the
g['seen'] dictionary.

If this is the case then the variable has already been written to the
output netCDF file and so we don't need to do it again.

If 'ncdims' is set then a extra condition for equality is applied,
namely that of 'ncdims' being equal to the netCDF dimensions (names
and order) to that of a variable in the g['seen'] dictionary.

When True is returned, the input variable is added to the g['seen']
dictionary.

Updates g['seen'].

:Parameters:

    variable : 

    ncdims : tuple, optional

    g : dict

:Returns:

    out : bool
        True if the variable has already been written to the file,
        False otherwise.

'''
        seen = g['seen']

        for key in seen:
            if ncdims is not None and ncdims != seen[key]['ncdims']:
                # The netCDF dimensions (names and order) of the input
                # variable are different to those of this variable in
                # the 'seen' dictionary
                continue

            # Still here?
            if variable.equals(seen[key]['variable']):
                seen[id(variable)] = {'variable': variable,
                                      'ncvar'   : seen[key]['ncvar'],
                                      'ncdims'  : seen[key]['ncdims']}
                return True
        #--- End: for

        return
    #--- End: def

    def _datatype(variable):
        '''

Return the netCDF4.createVariable datatype corresponding to the
datatype of the array stored (possibly virtually) in the input
variable's 'data' attribute. E.g. if variable.dtype is 'float32', then
'f4' will be returned.

Numpy string data types will return 'S1' regardless of the numpy
string length. This means that the required conversion of
multi-character datatype numpy arrays into single-character datatype
numpy arrays (with an extra trailing dimension) is expected to be done
elsewhere (currently in def _create_netcdf_variable).

If the input variable has no data attribute then 'S1' is returned.

'''
        if (variable.dtype is None     or
            variable.dtype.char == 'S' or
            not hasattr(variable, 'dtype')):
            return 'S1'            

        # Still here?
        return '%s%s' % (variable.dtype.kind, variable.dtype.itemsize)
    #--- End: def

    def _create_netcdf_variable(ncvar, ncdimensions, input, 
                                omit=(), extra={}, g=g):
        '''

Create a netCDF variable from 'input' with name 'ncvar' and dimensions
'ncdimensions'. The netCDF variable's properties are given by
input.properties(), less any given by the 'omit' argument. If a new
string-length netCDF dimension is required then it will also be
created. The 'seen' dictionary is updated for 'input'.

Updates g['strlenN'], g['ncdim_size'], g['netcdf'], g['nc'],
        g['seen'].

:Parameters:

    ncvar : str
        The netCDF name of the variable.

    ncdimensions : tuple
        The netCDF dimension names of the variable

    input : Variable
        The variable (coordinate, cell measure or field) to write to
        the file.

    omit : sequence, optional

    extra : dict, optional

    g : dict

:Returns:

    None

'''
        # Set the netCDF4.createVariable datatype
        datatype = _datatype(input)
        
        dimensions = ncdimensions
        
        if not input.hasData:
            data = None
        else:
            data      = input.Data
            order     = data.order
            direction = data.direction
            units     = data.Units
            save      = data.save_to_disk()
            
            if datatype == 'S1':
                # Convert a multi-character data type numpy array into
                # a single-character data type numpy array with an
                # extra trailing dimension.
                strlen = data.dtype.itemsize
                if strlen > 1:
                    
                    # Create a new dimension for the maximum string length
                    ncdim, g['strlenN'] = _check_name('strlen%(strlen)d' % locals(), 
                                                      g['strlenN'], dimsize=strlen)
                    
                    ncdim_size = g['ncdim_size']
                    if ncdim not in ncdim_size:
                        ncdim_size[ncdim] = strlen
                        g['netcdf'].createDimension(ncdim, strlen)
                    #--- End: if
                        
                    dimensions = ncdimensions + (ncdim,)
                     
                    data = data.copy()

                    for partition in data.partitions.flat():
                        
                        p_data = partition.conform(order, direction, units, save)
                        
                        temp = numpy.ma.empty((p_data.size, strlen), dtype='|S1')
                        for i, string in enumerate(p_data.flat):
                            temp[i,:] = list(string.ljust(strlen, ' ')) 

                        partition.data = numpy.ma.resize(temp, p_data.shape + (strlen,))

                        del temp

                        partition.order.append(ncdim)
                        partition.shape.append(strlen)
                        partition.location.append((0, strlen))
                        partition.direction[ncdim] = True

                        partition.close()
                    #--- End: for

                    data._shape += (strlen,)
                    data.order.append(ncdim)
                    data.direction[ncdim] = True

                    order     = data.order
                    direction = data.direction
                    save      = False
            #--- End: if
        #--- End: if

        # Find the fill value (note that this is set in the call to
        # netCDF4.createVariable, rather than with setncattr).
        fill_value = getattr(input, '_FillValue', None)

        # ------------------------------------------------------------
        # Create a new netCDF variable and set the _FillValue
        # ------------------------------------------------------------
        g['nc'][ncvar] = g['netcdf'].createVariable(ncvar, datatype, 
                                                    dimensions,
                                                    fill_value=fill_value)

        # Add simple properties (and units and calendar) to the netCDF
        # variable
        netcdf_attrs = input._simple_attributes()        
        for attr in ('units', 'calendar'):
            if hasattr(input, attr):
                netcdf_attrs[attr] = getattr(input, attr)
        #--- End: for

        netcdf_attrs.update(extra)
        for attr in omit:
            if attr in netcdf_attrs:
                netcdf_attrs.pop(attr) 
        #--- End: for

        if hasattr(g['nc'][ncvar], 'setncatts'):
            # Use the faster setncatts
            g['nc'][ncvar].setncatts(netcdf_attrs)
        else:
            # Otherwise use the slower setncattr
            for attr, value in netcdf_attrs.iteritems():
                g['nc'][ncvar].setncattr(attr, value)
        #--- End: if

        #-------------------------------------------------------------
        # Add data to the netCDF variable
        #
        # Note that we don't need to worry about scale_factor and
        # add_offset, since if a partition's data array is not a numpy
        # array, then it will have its own scale_factor and add_offset
        # parameters which will be applied when the array is realised,
        # and the python netCDF4 package will deal with the case when
        # scale_factor or add_offset are set as properties on the
        # variable.
        # -------------------------------------------------------------
        if data is not None:
            revert_to_file = True
            for partition in data.partitions.flat():
            
                data = partition.conform(order, direction, units, save,
                                         revert_to_file)

                g['nc'][ncvar][partition.indices] = data

# numpy.astype (bool -> int)

                partition.close()
            #--- End: for
        #--- End: if
    
        # Update the 'seen' dictionary
        g['seen'][id(input)] = {'variable': input,
                                'ncvar'   : ncvar,
                                'ncdims'  : ncdimensions,
                                }
    #--- End: def

    def _write_grid_mapping(transform, grid_mapping, g=g):
        '''

Write a grid mapping transform to the netCDF file.

Updates g['seen'].

:Parameters:

    transform : Transform
        The grid mapping transform to write to the file.

    grid_mapping : list

    g : dict

:Returns:

    grid_mapping : list
        The updated list of netCDF grid_mapping names.

**Examples**

'''
        if _seen(transform, g=g):
            # Use existing grid_mapping
            ncvar = g['seen'][id(transform)]['ncvar']

        else:
            # Create a new grid mapping
            ncvar = getattr(transform, 'ncvar', transform.name)
        
            ncvar, g['gmN'] = _check_name(ncvar, g['gmN']) 
                            
            g['nc'][ncvar] = g['netcdf'].createVariable(ncvar, 'S1', ())
            
            # Add properties from key/value pairs
            if hasattr(g['nc'][ncvar], 'setncatts'):
                # Use the faster setncatts
                g['nc'][ncvar].setncatts(transform)
            else:
                # Otherwise use the slower setncattr
                for attr, value in transform.iteritems():
                    g['nc'][ncvar].setncattr(attr, value)
            #--- End: if
            
            # Update the 'seen' dictionary. Note that grid mappings have
            # no dimensions.
            g['seen'][id(transform)] = {'variable': transform, 
                                        'ncvar'   : ncvar, 
                                        'ncdims'  : ()}
        #--- End: if

        grid_mapping.append(ncvar)

        return grid_mapping
    #--- End: def

    def _check_name(base, counter, dimsize=None, g=g):
        '''

:Parameters:

    base : 

    counter : int

    dimsize : int, optional

    g : dict

:Returns:

    ncvar : str

    counter : int

'''
        ncvar_names = g['ncvar_names']

        if dimsize is not None:
            if base in ncvar_names and dimsize == g['ncdim_size'][base]:
                return base, counter

        if base in ncvar_names:
            ncvar = '%(base)s_%(counter)d' % locals()
            while ncvar in ncvar_names:
                counter += 1
                ncvar = '%(base)s_%(counter)d' % locals()
        else:
            ncvar = base

        ncvar_names.add(ncvar)

        return ncvar, counter
    #--- End: def
    
    def _write_a_field(f, save_dim_to_ncdim=False, add_to_seen=False, g=g):
        '''

:Parameters:

    f : Field

    save_dim_to_ncdim : bool, optional

    add_to_seen : bool, optional

    g : dict

:Returns:

    None

'''
        seen = g['seen']

        if save_dim_to_ncdim:
            saved_dim_to_ncdim = g['dim_to_ncdim'].copy()
            
        if add_to_seen:
            id_f = id(f)
            org_f = f
            
        f = f.copy()

        space      = f.space
        dimensions = space.dimensions

        g['dim_to_ncdim'] = {}

        # Initialize the list of the field's auxiliary coordinates
        coordinates       = []

        for dim in sorted(space.dimension_sizes):
            if dim in space:
                # ----------------------------------------------------
                # A dimension coordinate exists for this dimension, so
                # create a netCDF dimension and a netCDF coordinate
                # variable.
                # ----------------------------------------------------
                if dimensions[dim][0] in dimensions['data']:
                    # The field's data array spans this dimension, so
                    # write it to the file as a 1-d dimension
                    # coordinate.
                    ncdim = _write_dimension_coordinate(f, dim, g=g)

                else:
                    # The field's data array does not span this
                    # dimension.
                    write_as_scalar_coordinate = True
                    for key in space.get_keys('^aux|^cm'):
                        if dim in dimensions[key]:
                            write_as_scalar_coordinate = False
                            break
                    #--- End: for

                    if write_as_scalar_coordinate:
                        # There are NO auxiliary coordinates/cell
                        # measures which span this dimension, so write
                        # to the file as a scalar coordinate.
                        coordinates = _write_scalar_coordinate(f, dim,
                                                               coordinates,
                                                               g=g)
                    else:
                        # There ARE auxiliary coordinates/cell
                        # measures which span this dimension, so it
                        # needs to be written to the file as a 1-d
                        # dimension coordinate and the field's data
                        # array needs to be expanded to include the
                        # dimension.
                        ncdim = _write_dimension_coordinate(f, dim, g=g)

                        f.expand_dims(dim)
                    #--- End: if
                
            else:
                # ----------------------------------------------------
                # There is no dimension coordinate for this dimension,
                # so just create a netCDF dimension if it spans the
                # field's data array.
                # ----------------------------------------------------
                if dim not in dimensions['data']:
                    # If there are auxiliary coordinates/cell measures
                    # which span this dimension, then expand the
                    # field's data array to include it.
                    for key in space.get_keys('^aux|^cm'):
                        if dim in dimensions[key]:
                            f.expand_dims(dim)
                            break
                #--- End: if

                # Create the new dimension if the field's data array
                # spans it
                if dim in dimensions['data']:
                    try:
                        ncdim = space.nc_dimensions[dim]
                    except (AttributeError, KeyError):
                        ncdim = 'dim'
                        
                    ncdim, g['dimN'] = _check_name(ncdim, g['dimN'])

                    _write_dimension(ncdim, f, dim, g=g)
                 #--- End: if
            #--- End: if

        #--- End: for
                
        # ------------------------------------------------------------
        # Create auxiliary coordinate variables, except those which
        # might be completely specified elsewhere by a transformation.
        # ------------------------------------------------------------
        # Initialize the list of 'coordinates' attribute variable
        # values (each of the form 'name')
        for key in sorted(space.get_keys('^aux')):
            coordinates = _write_auxiliary_coordinate(f, key,
                                                      coordinates,
                                                      g=g)
        #--- End: for

        # ------------------------------------------------------------
        # Create cell measures variables
        # ------------------------------------------------------------
        # Initialize the list of 'cell_measures' attribute values
        # (each of the form 'measure: name')
        cell_measures = []
        for key in sorted(space.get_keys('^cm')):            
            cell_measures = _write_cell_measure(f, key, cell_measures, g=g)

        # ------------------------------------------------------------
        # Create grid mapping and formula terms variables
        # ------------------------------------------------------------
        # Initialize the list of 'grid_mapping' attribute variable
        # values (each of the form 'name')
        grid_mapping  = []
        formula_terms = []
        for transform in space.transforms.values():
            if transform.isgrid_mapping: 
                # ------------------------------------------------
                # This transform is a grid mapping
                # ------------------------------------------------
                grid_mapping = _write_grid_mapping(transform, grid_mapping, g=g)

            else:
                # ------------------------------------------------
                # This transform is a formula terms
                # ------------------------------------------------

                key = space.coord(transform.name, exact=True, key=True)
                if key is None:
                    # This formula_terms doesn't match up with an
                    # existing coordinate
                    continue
                
                # Still here? Then create the formula_terms
                # attribute string
                for term, variable in transform.iteritems():
                    
                    if variable is None:
                        # Do not create broken formula_terms
                        formula_terms = None
                        break
                
                    if isinstance(variable, Variable):
                        if not _seen(variable, g=g):
                            # var is a variable which hasn't been
                            # written yet (set save=True so we
                            # don't lose the current the
                            # dim_to_ncdim dictionary).
                            _write_a_field(variable, save_dim_to_ncdim=True,
                                           add_to_seen=True, g=g)
                    elif variable in space:
                        # var is a key of the space, such as 'dim1'
                        variable = space[variable]
                    else:
                        # Do not create broken formula_terms
                        formula_terms = None
                        break
                    #--- End: if
                   
                    # Record the netCDF variable name for the
                    # formula_terms string
                    ncvar = seen[id(variable)]['ncvar']
                    formula_terms.append('%(term)s: %(ncvar)s' % locals())
                #--- End: for
        
                # Add the formula_terms attribute to the output
                # variable
                if formula_terms:
                    ncvar = seen[id(space[key])]['ncvar']
                    g['nc'][ncvar].setncattr('formula_terms', ' '.join(formula_terms))
            #--- End: if

        #--- End: for

        # ------------------------------------------------------------
        # Create ancillary variables
        # ------------------------------------------------------------
        # Initialize the list of 'ancillary_variables' attribute
        # values
        ancillary_variables = []
        if hasattr(f, 'ancillary_variables'):
            for av in f.ancillary_variables:
                _write_a_field(av, save_dim_to_ncdim=True, add_to_seen=True,
                               g=g)
                ancillary_variables.append(seen[id(av)]['ncvar'])
        #--- End: if

        # ------------------------------------------------------------
        # Create the data variable
        # ------------------------------------------------------------
        ncvar = getattr(f, 'ncvar', f.name(default='data'))
            
        ncvar, g['dataN'] = _check_name(ncvar, g['dataN'])

        dim_to_ncdim    = g['dim_to_ncdim']
        dim_to_ncscalar = g['dim_to_ncscalar']

        ncdimensions = tuple(dim_to_ncdim[dim] 
                             for dim in space.dimensions['data'])

        extra = {}

        # Cell measures
        if cell_measures:
            extra['cell_measures'] = ' '.join(cell_measures)           

        # Auxiliary/scalar coordinates
        if coordinates:
            extra['coordinates'] = ' '.join(coordinates)

        # Grid mapping
        if grid_mapping: 
            extra['grid_mapping']  = ' '.join(grid_mapping)

        # Ancillary variables
        if ancillary_variables:
            extra['ancillary_variables'] = ' '.join(ancillary_variables)
            
        # Flag values
        if hasattr(f, 'flag_values'):
            extra['flag_values'] = f.flag_values

        # Flag masks
        if hasattr(f, 'flag_masks'):
            extra['flag_masks'] = f.flag_masks

        # Flag meanings
        if hasattr(f, 'flag_meanings'):
            extra['flag_meanings'] = ' '.join(f.flag_meanings)

        # Cell methods
        if hasattr(f, 'cell_methods'):
            cell_methods = f.cell_methods.copy()

            for cm in cell_methods:
                if cm['name'] == ['area']:
                    continue

                # Still here?
                for i, dim in enumerate(cm['dim']):
                    if dim in dim_to_ncdim:
                        cm['name'][i] = dim_to_ncdim[dim]
                    elif dim in dim_to_ncscalar:
                        cm['name'][i] = dim_to_ncscalar[dim]
            #--- End: for

            extra['cell_methods'] = ' '.join(cell_methods.strings())
        #--- End: if

        # Create a new data variable
        _create_netcdf_variable(ncvar, ncdimensions, f,
                                omit  = g['global_properties'],
                                extra = extra,
                                g=g)
        
        # ------------------------------------------------------------
        # If we saved the original dim_to_ncdim then reinstate it
        # ------------------------------------------------------------
        if save_dim_to_ncdim:
            g['dim_to_ncdim'] = saved_dim_to_ncdim

        # Update the 'seen' dictionary, if required
        if add_to_seen:
            g['seen'][id_f] = {'variable': org_f,
                               'ncvar'   : ncvar,
                               'ncdims'  : ncdimensions}
    #--- End: def
            
    def _create_global_properties(fields, g=g):
        '''

Find the netCDF global properties from all of the input fields and
write them to the netCDF4.Dataset.

Updates g['global_properties'].

:Parameters:

    fields : FieldList

    g : dict

:Returns:

    None

'''
        # Data variable properties, as defined in Appendix A
        data_properties = set(('add_offset',
                               'ancillary_variables',
                               'cell_measures',
                               'cell_methods',
                               'coordinates',
                               '_FillValue',
                               'flag_masks',
                               'flag_meanings',
                               'flag_values',
                               'grid_mapping',
                               'long_name',
                               'missing_value',
                               'scale_factor',
                               'standard_error_multiplier',
                               'standard_name',
                               'units',
                               'valid_max',
                               'valid_min',
                               'valid_range',
                               ))

        # Global properties, as defined in Appendix A
        global_properties = set(('comment',
                                 'Conventions',
                                 'history',
                                 'institution',
                                 'references',
                                 'source',
                                 'title',
                                 ))

        # Put all non-standard CF properties (i.e. those not in the
        # data_properties set) into the global_properties set.
        for f in fields:
            for attr in set(f._simple_attributes()) - global_properties:
                if attr not in data_properties:
                    global_properties.add(attr)
        #--- End: for

        # Remove properties from the new global_properties set which
        # have different values in different fields
        f0 = fields[0]
        for prop in tuple(global_properties):
            if not f0.hasprop(prop):
                global_properties.remove(prop)
                continue
                
            prop0 = f0.getprop(prop)

            if len(fields) > 1:
                for f in fields[1:]:
                    if (not f.hasprop(prop) or 
                        not utils_equals(f.getprop(prop), prop0, traceback=False)):
                        global_properties.remove(prop)
                        break
        #--- End: if

        # Write the global properties to the file
        g['netcdf'].setncattr('Conventions', __Conventions__)

        for attr in global_properties - set(('Conventions',)):
            g['netcdf'].setncattr(attr, f0.getprop(attr)) 

        g['global_properties'] = global_properties
    #--- End: def

    # ---------------------------------------------------------------
    # Open the output netCDF file
    # ---------------------------------------------------------------
    g['netcdf'] = netCDF4.Dataset(file, 'w', format=format)

    # ---------------------------------------------------------------
    # Set the fill mode for a Dataset open for writing to off. This
    # will prevent the data from being pre-filled with fill values,
    # which may result in some performance improvements.
    # ---------------------------------------------------------------
    g['netcdf'].set_fill_off()

    # ---------------------------------------------------------------
    # Flatten an input list or tuple of fields and field lists into a
    # single field list
    # ---------------------------------------------------------------
    if isinstance(fields, (list, tuple)): 
        fields = [f for element in fields for f in element]
#        temp = FieldList()
#        for f in fields:
#            temp.extend(f)#
#
#        fields = temp
    #--- End: if

    # ---------------------------------------------------------------
    # Write global properties to the file first. This is important as
    # doing it later can slows things down enormously. This function
    # also creates the g['global_properties'] set, which is used in
    # the _write_a_field function.
    # ---------------------------------------------------------------
    _create_global_properties(fields, g=g)

    # ---------------------------------------------------------------
    # ---------------------------------------------------------------
    for f in fields:
        _write_a_field(f, g=g)
        if verbose:
            print repr(f)
    #-- End: for

    # ---------------------------------------------------------------
    # Write all of the buffered data to disk
    # ---------------------------------------------------------------
    g['netcdf'].close()

    if verbose:
        n = len(fields)
        print 'Wrote',repr(n),'field'+('s' if n>1 else '')+' to',file
#--- End: def
